let marbleRaceEngineMatter0190 = {
	trueRandom: true,
	marbleForce: undefined,
	random: function (min, max) {
		return min + Math.random() * (max - min)
	},
	seed: function (seed) {
		// random seed is not used
	},
	unpassMarble: function (marbleID) {
		let stat = this.marbleStatus[marbleID]
		if (stat.stopTime > 0)
			return

		this.marbleStatus.lastStatus = marbleRaceConfig.constant.UNFINISHED
		this.marbleStatus.lastStatusMarble = marbleID

		if (stat.status == marbleRaceConfig.constant.PASSED)
			++this.goalRes
		stat.status = marbleRaceConfig.constant.UNFINISHED

		this.setPosition(this.circleArray[marbleID], [this.startX, this.startY])
	},
	passMarble: function (marbleID) {
		let stat = this.marbleStatus[marbleID]
		if (stat.stopTime > 0)
			return

		if (!this.hidePassedMessage) {
			this.marbleStatus.lastStatus = marbleRaceConfig.constant.PASSED
			this.marbleStatus.lastStatusMarble = marbleID
		}

		stat.status = marbleRaceConfig.constant.PASSED
		stat.passTime = this.time
		if (this.level.scoreBonus !== undefined)
			stat.score += this.level.scoreBonus
		else
			++stat.score
		if (this.passBonus) {
			stat.value += this.passBonusValue
			if (this.passBonus == 'increase')
				++this.passBonusValue
		}

		--this.goalRes
		if (this.passedBonus == 'thisPenultimate') {
			if (!this.goalRes)
				this.bonusMarble(marbleID)
		}
	},
	promoteMarble: function (marbleID) {
		if (!this.hidePromotedMessage) {
			this.marbleStatus.lastStatus = marbleRaceConfig.constant.PROMOTED
			this.marbleStatus.lastStatusMarble = marbleID
		}

		let stat = this.marbleStatus[marbleID]
		stat.status = marbleRaceConfig.constant.PROMOTED
		stat.passTime = this.time
		if (this.level.scoreBonus !== undefined)
			stat.score += this.level.scoreBonus
		else
			++stat.score
	},
	bonusMarble: function (marbleID) {
		if (this.marbleStatus[marbleID].status == marbleRaceConfig.constant.PASSED) {
			this.promoteMarble(marbleID)
		} else {
			this.passMarble(marbleID)
			this.setPosition(this.circleArray[marbleID], [this.passX, this.passY])
		}
	},
	stopMarble: function (marbleID) {
		if (this.marbleStatus[marbleID].status != marbleRaceConfig.constant.PASSED) {
			let stat = this.marbleStatus[marbleID]
			if (stat.stopTime)
				stat.stopTime += 5
			else {
				stat.stopTime = 5
				stat.stopPosition = this.getPosition(this.circleArray[marbleID])
			}
		}
	},
	eliminateMarble: function (marbleID, sacrificed = false) {
		if (sacrificed)
			--this.sacrificeRes

		if (sacrificed)
			this.marbleStatus.lastStatus = marbleRaceConfig.constant.SACRIFICED
		else
			this.marbleStatus.lastStatus = marbleRaceConfig.constant.ELIMINATED
		this.marbleStatus.lastStatusMarble = marbleID

		if (!this.circleArray[marbleID].marbleRaceIsRemoved)
			this.remove(this.circleArray[marbleID])

		let stat = this.marbleStatus[marbleID]
		stat.status = marbleRaceConfig.constant.ELIMINATED
		let r = --this.marbleStatus.totalRank
		if (this.rankAssign)
			stat.rank = this.rankAssign[r]
		else
			stat.rank = r
		if (this.scoreAssign)
			stat.score = this.scoreAssign[r]
		if (this.eliminatedBonus == 'team') {
			let team = this.marbleConfigColor[marbleID].team
			for (let i = 0; i < this.n; ++i)
				if (i != marbleID && this.marbleConfigColor[i].team == team && this.marbleStatus[i].enabled)
					this.bonusMarble(i)
		} else if (this.eliminatedBonus == 'teamStop') {
			let team = this.marbleConfigColor[marbleID].team
			for (let i = 0; i < this.n; ++i)
				if (i != marbleID && this.marbleConfigColor[i].team == team && this.marbleStatus[i].enabled)
					this.stopMarble(i)
		}
	},
	eliminateMarbleIfNeeded: function (marbleID) {
		let eliminated = this.valueThreshold !== undefined && this.marbleStatus[marbleID].value <= this.valueThreshold
		if (eliminated)
			this.eliminateMarble(marbleID)
		return eliminated
	},
	winMarble: function (marbleID) {
		this.marbleStatus.lastStatus = marbleRaceConfig.constant.WIN
		this.marbleStatus.lastStatusMarble = marbleID

		let stat = this.marbleStatus[marbleID]
		stat.status = marbleRaceConfig.constant.WIN
		console.log('win', marbleID, stat.marbleStatus)
		let r = --this.marbleStatus.totalRank
		if (this.rankAssign)
			stat.rank = this.rankAssign[r]
		else
			stat.rank = r
		if (this.scoreAssign)
			stat.score = this.scoreAssign[r]
	},
	remove: function (body) {
		Matter.Composite.remove(this.engine.world, body, true)
		body.marbleRaceIsRemoved = true
	},
	changeGravity: function (lambda = -1) {
		this.engine.gravity.scale *= lambda
	},
	setColor: function (body, color, size) {
		if (!body.marbleRaceIsRemoved) {
			let colorCalculated = marbleRaceColorUtility.calculateColor(color, size)
			for (let i = 0; i < body.parts.length; ++i) {
				let part = body.parts[i]

				if (colorCalculated.sprite) {
					if (!part.render.sprite)
						part.render.sprite = {}
					part.render.sprite.texture = colorCalculated.sprite
					part.render.sprite.xOffset = .5
					part.render.sprite.yOffset = .5
					part.render.sprite.xScale = colorCalculated.spriteScale
					part.render.sprite.yScale = colorCalculated.spriteScale

					part.render.lineWidth = undefined
					part.render.fillStyle = undefined
					part.render.strokeStyle = undefined
				} else {
					part.render.sprite = undefined

					part.render.lineWidth = colorCalculated.lineWidth
					part.render.fillStyle = colorCalculated.fill
					part.render.strokeStyle = colorCalculated.stroke
				}
			}
		}
	},
	setAngle: function (body, theta) {
		if (!body.marbleRaceIsRemoved)
			for (let i = 0; i < body.parts.length; ++i) {
				let part = body.parts[i]
				Matter.Body.setAngle(part, theta)
			}
	},
	setMass: function (body, mass) {
		if (!body.marbleRaceIsRemoved)
			Matter.Body.setMass(body, mass)
	},
	getPosition: function (body) {
		let pos = body.position
		return [pos.x, pos.y]
	},
	setPosition: function (body, pos) {
		if (!body.marbleRaceIsRemoved)
			Matter.Body.setPosition(body, Matter.Vector.create(pos[0], pos[1]))
	},
	getVelocity: function (body) {
		let v = body.velocity
		return [v.x, v.y]
	},
	setVelocity: function (body, v) {
		if (!body.marbleRaceIsRemoved)
			Matter.Body.setVelocity(body, { x: v[0], y: v[1] })
	},
	setStatic: function (body) {
		Matter.Body.setStatic(body, true)
	},
	setMovable: function (body) {
		Matter.Body.setStatic(body, false)
	},
	beforeRender: function (event) {
		// Set time
		this.time = event.timestamp * .001

		// Update color
		for (let i = 0; i < this.objectArray.length; ++i) {
			let item = this.objectArray[i]
			if (!item.body.marbleRaceIsRemoved) {
				let color = item.color
				if (this.level.type == 'elimination' && item.isGoal) {
					if (this.sacrificeRes)
						color = marbleRaceConfig.theme.sacrifice
					else if (!this.goalRes)
						color = marbleRaceConfig.theme.eliminate
				}
				this.setColor(item.body, color, item.size)
			}
		}
	},
	afterRender: function (event) {
		let ctx = marbleRaceConfig.canvasContext

		// Set time
		this.time = event.timestamp * .001

		// Draw launch circle
		if (this.time < this.launchTime) {
			let r = this.launchRadius
			ctx.strokeStyle = marbleRaceConfig.theme.launch
			ctx.beginPath()
			ctx.lineWidth = marbleRaceConfig.theme.launchBorderWidth
			ctx.ellipse(this.startX, this.startY, r, r, 0, 0, 2 * Math.PI);
			ctx.stroke()
		}

		// Draw trail
		if (marbleRaceConfig.theme.showTrail) {
			let len = marbleRaceConfig.theme.trailLen
			for (let i = 0; i < this.n; ++i)
				if (this.circleArray[i] !== undefined && this.marbleConfigColor[i].showTrail !== false) {
					let body = this.circleArray[i]
					if (!body.marbleRaceIsRemoved) {
						let colorCalculated = marbleRaceColorUtility.calculateColor(this.marbleConfigColor[i].color, this.marbleSizeArray[i])
						let pos = this.getPosition(body)
						let trail = this.trailArray[i]
						trail.unshift(pos)
						while (trail.length > len)
							trail.pop()

						ctx.strokeStyle = colorCalculated.fill + '3F'
						ctx.lineWidth = body.circleRadius * 2
						ctx.lineCap = 'round'
						ctx.lineJoin = 'round'
						ctx.moveTo(trail[0][0], trail[0][1])
						for (let j = 1; j < trail.length; ++j) {
							ctx.beginPath()
							ctx.lineTo(trail[j][0], trail[j][1])
							ctx.stroke()
						}
						ctx.strokeStyle = colorCalculated.fill
						ctx.beginPath()
						ctx.stroke()
					}
				}
		}

		// Draw stoptime
		if (marbleRaceConfig.theme.showStopTime) {
			let color = marbleRaceConfig.theme.stop
			let scale = marbleRaceConfig.theme.stopRadiusScale
			let len = marbleRaceConfig.theme.trailLen
			for (let i = 0; i < this.n; ++i)
				if (this.circleArray[i] !== undefined && this.marbleStatus[i].stopTime > 0) {
					let body = this.circleArray[i]
					if (!body.marbleRaceIsRemoved) {
						let colorCalculated = marbleRaceColorUtility.calculateColor(this.marbleConfigColor[i].color, this.marbleSizeArray[i])
						let pos = this.getPosition(body)

						ctx.strokeStyle = color
						ctx.beginPath()
						let width = this.marbleStatus[i].stopTime * scale
						let radiusOfBorder = body.circleRadius + .5 * width
						ctx.lineWidth = width
						ctx.ellipse(pos[0], pos[1], radiusOfBorder, radiusOfBorder, 0, 0, 2 * Math.PI);
						ctx.stroke()
					}
				}
		}

		// Draw value
		if (this.marbleStatus.showValue) {
			ctx.font = marbleRaceConfig.theme.fontValue
			ctx.textAlign = 'start'
			ctx.textBaseline = 'alphabetic'
			let offset = marbleRaceConfig.theme.textValueOffset
			for (let i = 0; i < this.n; ++i)
				if (this.marbleStatus[i].status == marbleRaceConfig.constant.UNFINISHED) {
					let body = this.circleArray[i]
					if (!body.marbleRaceIsRemoved) {
						let colorCalculated = marbleRaceColorUtility.calculateColor(this.marbleConfigColor[i].color, this.marbleSizeArray[i])
						let pos = this.getPosition(body)
						let value = Math.floor(this.marbleStatus[i].value)

						ctx.fillStyle = colorCalculated.stroke
						ctx.fillText(value.toString(), pos[0] + offset[0], pos[1] + offset[1])
					}
				}
		}

		// Draw text
		ctx.font = marbleRaceConfig.theme.fontText
		ctx.textAlign = 'center'
		ctx.textBaseline = 'middle'
		for (let i = 0; i < this.textArray.length; ++i) {
			let text = this.textArray[i]
			let colorCalculated = marbleRaceColorUtility.calculateColor(text.color)
			ctx.fillStyle = colorCalculated.stroke
			ctx.fillText(text.content, text.x, text.y)
		}

		// Draw object text
		ctx.font = marbleRaceConfig.theme.fontText
		ctx.textAlign = 'center'
		ctx.textBaseline = 'middle'
		for (let i = 0; i < this.objectArray.length; ++i)
			if (this.objectArray[i].text && !this.objectArray[i].body.marbleRaceIsRemoved) {
				let pos = this.getPosition(this.objectArray[i].body)
				let text = this.objectArray[i].text
				let colorCalculated = marbleRaceColorUtility.calculateColor(text.color)
				if (text.offset) {
					pos[0] += text.offset[0]
					pos[1] += text.offset[1]
				}
				ctx.fillStyle = colorCalculated.stroke
				ctx.fillText(text.content, pos[0], pos[1])
			}
	},
	beforeUpdate: function (event) {
		// Set time
		this.time = event.timestamp * .001

		// Before update
		for (let i = 0; i < this.objectArray.length; ++i) {
			let object = this.objectArray[i]
			if (object.onBeforeUpdate)
				object.onBeforeUpdate(object, this)
		}

		// Additional force between marbles
		if (this.marbleForce) {
			if (this.marbleForce.type == 'gravitation') {
				let c = this.marbleForce.constant
				let k = this.marbleForce.exponent
				let minDis = this.marbleForce.minDis
				let maxDis = this.marbleForce.maxDis
				for (let i = 1; i < this.n; ++i)
					if (this.circleArray[i] !== undefined) {
						let bodyI = this.circleArray[i]
						let posI = bodyI.position
						for (let j = 0; j < i; ++j)
							if (this.circleArray[i] !== undefined) {
								let bodyJ = this.circleArray[j]
								let posJ = bodyJ.position

								let dx = posI.x - posJ.x
								let dy = posI.y - posJ.y
								let x = Math.sqrt(dx * dx + dy * dy)
								if (x <= maxDis && x >= minDis) {
									let lambda = c
									if (c && k != 1)
										lambda *= Math.pow(x, k - 1)
									let forceX = lambda * dx
									let forceY = lambda * dy

									Matter.Body.applyForce(bodyI, posI, Matter.Vector.create(-forceX, -forceY))
									Matter.Body.applyForce(bodyJ, posJ, Matter.Vector.create(forceX, forceY))
								}
							}
					}
			}
		}

		// Moving goal
		if (this.goalMoving) {
			if (this.goalMoving.type == 'random') {
				this.goalMovingX += this.random(-this.goalMoving.speed, this.goalMoving.speed)
				if (this.goalMovingX > this.goalMoving.speedMax) {
					this.goalMovingX = this.goalMoving.speedMax
				} else if (this.goalMovingX < -this.goalMoving.speedMax) {
					this.goalMovingX = -this.goalMoving.speedMax
				}

				this.goalMovingY += this.random(-this.goalMoving.speed, this.goalMoving.speed)
				if (this.goalMovingY > this.goalMoving.speedMax) {
					this.goalMovingY = this.goalMoving.speedMax
				} else if (this.goalMovingY < -this.goalMoving.speedMax) {
					this.goalMovingY = -this.goalMoving.speedMax
				}

				for (let i = 0; i < this.objectArray.length; ++i) {
					let collider = this.objectArray[i]
					if (!collider.body.marbleRaceIsRemoved && collider.isGoal) {
						let pos = this.getPosition(collider.body)

						let newX = pos[0] + this.goalMovingX
						if (newX < this.goalMoving.xMin)
							newX = this.goalMoving.xMin
						if (newX > this.goalMoving.xMax)
							newX = this.goalMoving.xMax
						/*
						while (newX < this.goalMoving.xMin)
							newX += this.goalMoving.xMax - this.goalMoving.xMin
						while (newX > this.goalMoving.xMax)
							newX -= this.goalMoving.xMax - this.goalMoving.xMin*/

						let newY = pos[1] + this.goalMovingY
						if (newY < this.goalMoving.yMin)
							newY = this.goalMoving.yMin
						if (newY > this.goalMoving.yMax)
							newY = this.goalMoving.yMax
						/*
						while (newY < this.goalMoving.yMin)
							newY += this.goalMoving.yMax - this.goalMoving.yMin
						while (newY > this.goalMoving.yMax)
							newY -= this.goalMoving.yMax - this.goalMoving.yMin*/

						let attractionX = 0
						let attractionY = 0
						let attractionCnt = 0
						for (let j = 0; j < this.n; ++j) {
							let circle = this.circleArray[i]
							if (circle) {
								let stat = this.marbleStatus[i]
								if (stat.status == marbleRaceConfig.constant.UNFINISHED) {
									let pos = this.getPosition(circle)
									let dx = pos[0] - newX
									let dy = pos[1] - newY
									let lambda = this.goalMoving.attraction * Math.pow(dx * dx + dy * dy, -.5)
									attractionX += lambda * dx
									attractionY += lambda * dy
									++attractionCnt
								}
							}
						}
						if (attractionCnt) {
							newX += attractionX / attractionCnt
							newY += attractionY / attractionCnt
						}

						this.setPosition(collider.body, [newX, newY])
					}
				}
			}
		}
	},
	afterUpdate: function (event) {
		// Set time
		this.time = event.timestamp * .001
		let deltaTime = this.time - this.lastTime
		this.lastTime = this.time

		// Handle launch time
		if (this.time < this.launchTime) {
			let r2 = this.launchRadius * this.launchRadius
			for (let i = 0; i < this.n; ++i) {
				let circle = this.circleArray[i]
				if (circle) {
					let stat = this.marbleStatus[i]
					if (stat.status == marbleRaceConfig.constant.UNFINISHED || (stat.status == marbleRaceConfig.constant.ELIMINATED && this.enableEliminatedMarbles)) {
						let pos = this.getPosition(circle)
						let dx = pos[0] - this.startX
						let dy = pos[1] - this.startY
						if (dx * dx + dy * dy > r2) {
							this.setPosition(circle, [this.startX, this.startY])
							let theta = this.random(0, Math.PI + Math.PI)
							Matter.Body.setVelocity(circle, Matter.Vector.create(this.launchVelocity * Math.cos(theta), this.launchVelocity * Math.sin(theta)))
						}
					}
				}
			}
		}

		// Handle stop time
		for (let i = 0; i < this.n; ++i) {
			let circle = this.circleArray[i]
			if (circle) {
				let stat = this.marbleStatus[i]
				if (stat.stopTime) {
					let time = stat.stopTime - deltaTime
					if (time > 0) {
						this.setPosition(this.circleArray[i], stat.stopPosition)
						this.setVelocity(this.circleArray[i], [0, 0])
						stat.stopTime = time
					} else
						stat.stopTime = undefined
				}
			}
		}

		// Value update
		if (this.level.type == 'value' && this.level.valueIncrement !== undefined) {
			let inc = deltaTime * this.level.valueIncrement
			for (let i = 0; i < this.n; ++i) {
				let stat = this.marbleStatus[i]
				if (stat.enabled && stat.value && stat.status == marbleRaceConfig.constant.UNFINISHED) {
					stat.value += inc
					this.eliminateMarbleIfNeeded(i)
				}
			}
		}

		// Detect marble collision
		for (let i = 0; i < this.objectArray.length; ++i) {
			let collider = this.objectArray[i]
			if (collider.hasCollision && !collider.body.marbleRaceIsRemoved)
				for (let j = 0; j < this.n; ++j)
					if (this.circleArray[j] !== undefined) {
						let circle = this.circleArray[j]
						let collision = Matter.Collision.collides(collider.body, circle)

						if (collision) {
							let portalID = collider.toPortal
							if (portalID) {
								let portal = this.portalMap[portalID]
								if (this.level.type == 'elimination' && collider.isGoal) {
									if (this.goalRes && this.marbleStatus[j].enabled)
										Matter.Body.setPosition(circle, portal.position)
									else
										Matter.Body.setPosition(circle, Matter.Vector.create(this.startX, this.startY))
								} else {
									Matter.Body.setPosition(circle, portal.position)
								}
							}
							this.collisionQueue.push({ marbleID: j, collider: collider })
						}
					}
		}

		// Handle marble collision events
		while (this.collisionQueue.length) {
			let item = this.collisionQueue.pop()
			let marbleID = item.marbleID
			let collider = item.collider
			let stat = this.marbleStatus[marbleID]

			if (stat.status == marbleRaceConfig.constant.UNFINISHED) {
				if (collider.toPortal !== undefined && !collider.isGoal && this.otherPortalBonusValue) {
					stat.value += this.otherPortalBonusValue
					this.eliminateMarbleIfNeeded(marbleID)
				}
				if (this.level.type == 'elimination') {
					if (collider.toPortal && !collider.isGoal && this.goalRes == 1 && this.portalUnpass !== undefined) {
						console.log(this.portalUnpass)
						if (!(--this.portalUnpass)) {
							console.log('qwq')
							console.log(this.marbleStatus)
							let unpassMarbles = []
							for (let i = 0; i < this.n; ++i) {
								let status = this.marbleStatus[i]
								if (status.enabled && status.status == marbleRaceConfig.constant.PASSED)
									unpassMarbles.push(i)
							}
							console.log(unpassMarbles)
							if (unpassMarbles.length) {
								let marbleID = unpassMarbles[Math.floor(Math.random() * unpassMarbles.length)]
								console.log(marbleID)
								this.unpassMarble(marbleID)
							}
						}
					}
					if (collider.isSacrifice) {
						if (this.sacrificeRes)
							this.eliminateMarble(marbleID, true)
					} else if (collider.isGoal) {
						if (this.sacrificeRes)
							this.eliminateMarble(marbleID, true)
						else
							this.passMarble(marbleID)
					}
				} else if (this.level.type == 'value') {
					if (collider.valueBonus) {
						stat.value += collider.valueBonus
						this.eliminateMarbleIfNeeded(marbleID)
					}
					if (collider.valueMultiply !== undefined) {
						stat.value *= collider.valueMultiply
						this.eliminateMarbleIfNeeded(marbleID)
					}
					if (collider.isGoal) {
						stat.value += this.level.valueBonus
						if (!this.eliminateMarbleIfNeeded(marbleID))
							this.passMarble(marbleID)
					}
				}
			}

			if (collider.onCollide) {
				collider.onCollide(collider, marbleID, this)
			}
		}

		// check if the level should be ended
		if (this.isRunning) {
			let end = true
			if (this.level.type == 'elimination') {
				let has_unfinished = false
				for (let i = 0; i < this.n; ++i)
					if (this.marbleStatus[i].enabled && this.marbleStatus[i].status == marbleRaceConfig.constant.UNFINISHED) {
						has_unfinished = true
						break
					}
				if (this.goalRes > 0 && has_unfinished)
					end = false
				if (!end && this.minPass) {
					let tmp = this.minPass
					for (let i = 0; i < this.n; ++i)
						if (!this.marbleStatus[i].enabled || this.marbleStatus[i].status == marbleRaceConfig.constant.ELIMINATED)
							++tmp
					if (tmp >= this.n) {
						for (let i = 0; i < this.n; ++i)
							if (this.marbleStatus[i].enabled && this.marbleStatus[i].status == marbleRaceConfig.constant.UNFINISHED)
								this.passMarble(i)
						end = true
					}
				}
			} else if (this.level.type == 'rest') {
				if (this.time < this.level.restTime)
					end = false
			} else if (this.level.type == 'value') {
				for (let i = 0; i < this.n; ++i)
					if (this.marbleStatus[i].enabled && this.marbleStatus[i].status == marbleRaceConfig.constant.UNFINISHED) {
						end = false
						break
					}
				if (!end && this.level.minPass) {
					let tmp = this.level.minPass
					for (let i = 0; i < this.n; ++i)
						if (!this.marbleStatus[i].enabled || this.marbleStatus[i].status == marbleRaceConfig.constant.ELIMINATED)
							++tmp
					if (tmp >= this.n) {
						for (let i = 0; i < this.n; ++i)
							if (this.marbleStatus[i].enabled && this.marbleStatus[i].status == marbleRaceConfig.constant.UNFINISHED)
								this.passMarble(i)
						end = true
					}
				}
			} else if (this.level.type == 'rotate') {
				end = false
			}
			if (end)
				this.end()
		}
	},
	end: function () {
		if (this.level.type == 'elimination') {
			for (let i = 0; i < this.n; ++i) {
				let stat = this.marbleStatus[i]
				if (stat.enabled && stat.status == marbleRaceConfig.constant.UNFINISHED)
					this.eliminateMarble(i)
			}
		}
		this.isRunning = false
		if (this.onEnd)
			this.onEnd()
	},
	del: function () {
		Matter.Render.stop(this.render)
		Matter.Runner.stop(this.runner)
	},
	start: function (level, config, status, onEnd) {
		this.time = 0
		this.lastTime = 0

		this.isRunning = true
		this.onEnd = onEnd

		this.level = level
		this.marbleConfigTeam = config.teams
		this.marbleConfigColor = config.colors
		this.marbleStatus = status
		this.marbleStatus.lastStatus = marbleRaceConfig.constant.UNKNOWN
		this.n = this.marbleConfigColor.length

		if (this.level.valueThreshold)
			this.valueThreshold = this.level.valueThreshold
		else if (this.defaultValueThreshold)
			this.valueThreshold = this.defaultValueThreshold
		else
			this.valueThreshold = 0

		if (this.level.minPass)
			this.minPass = this.level.minPass
		else if (this.defaultMinPass)
			this.minPass = this.defaultMinPass
		else
			this.minPass = 1

		if (this.goalMoving) {
			if (this.goalMoving.type == 'random') {
				this.goalMovingX = 0
				this.goalMovingY = 0
			}
		}

		if (this.level.type == 'elimination') {
			for (let i = 0; i < this.n; ++i)
				this.marbleStatus[i].enabled = (this.marbleStatus[i].status != marbleRaceConfig.constant.ELIMINATED)

			let eliminated = this.eliminated
			if (eliminated === undefined)
				eliminated = this.level.eliminated

			let sacrificed = this.sacrificed
			if (sacrificed === undefined) {
				sacrificed = this.level.sacrificed
				if (sacrificed === undefined)
					sacrificed = 0
			}

			this.sacrificeRes = sacrificed
			this.goalRes = -(eliminated + sacrificed)

			for (let i = 0; i < this.n; ++i)
				if (this.marbleStatus[i].enabled)
					++this.goalRes

			for (let i = 0; i < this.n; ++i)
				if (this.marbleStatus[i].enabled) {
					if (this.marbleStatus[i].status == marbleRaceConfig.constant.PROMOTED)
						this.passMarble(i)
					else
						this.marbleStatus[i].status = marbleRaceConfig.constant.UNFINISHED
				}
		} else if (this.level.type == 'value') {
			for (let i = 0; i < this.n; ++i)
				this.marbleStatus[i].enabled = (this.marbleStatus[i].status != marbleRaceConfig.constant.ELIMINATED)

			for (let i = 0; i < this.n; ++i)
				if (this.marbleStatus[i].enabled)
					this.marbleStatus[i].status = marbleRaceConfig.constant.UNFINISHED
		} else if (this.level.type == 'champion') {
			for (let i = 0; i < this.n; ++i)
				this.marbleStatus[i].enabled = (this.marbleStatus[i].status != marbleRaceConfig.constant.ELIMINATED)

			for (let i = 0; i < this.n; ++i)
				if (this.marbleStatus[i].enabled)
					this.winMarble(i)
		} else {
			for (let i = 0; i < this.n; ++i)
				this.marbleStatus[i].enabled = (this.marbleStatus[i].status != marbleRaceConfig.constant.ELIMINATED)
		}

		this.engine = Matter.Engine.create()
		this.engine.gravity.scale = marbleRaceConfig.gravity
		this.render = Matter.Render.create({
			canvas: marbleRaceConfig.canvas,
			engine: this.engine,
			options: {
				wireframes: false,
				background: marbleRaceConfig.theme.background
			}
		})

		this.objectArray = []
		this.bodyArray = []
		this.circleArray = []
		this.textArray = []

		this.trailArray = []
		this.marbleSizeArray = []

		this.portalMap = {}

		this.collisionQueue = []

		for (let i = 0; i < this.level.objects.length; ++i) {
			let object = this.level.objects[i]
			if (object.type == 'object') {
				let body;
				let size;
				if (object.shape == 'rect') {
					body = Matter.Bodies.rectangle(object.x, object.y, object.width, object.height)
					size = Math.max(object.width, object.height)
				} else if (object.shape == 'circle') {
					body = Matter.Bodies.circle(object.x, object.y, object.radius)
					size = object.radius + object.radius
				} else if (object.shape == 'polygon') {
					body = Matter.Bodies.fromVertices(object.x, object.y, object.vertices)
					size = object.size
				}

				this.objectArray.push({
					body: body,
					color: object.color,
					size: size,
					text: object.text,
					onBeforeUpdate: object.onBeforeUpdate,
					onCollide: object.onCollide,
					toPortal: object.toPortal,
					isGoal: object.isGoal,
					isSacrifice: object.isSacrifice,
					valueMultiply: object.valueMultiply,
					valueBonus: object.valueBonus,
					hasCollision: (!!object.toPortal) || (!!object.onCollide) || (!!object.isGoal) || (!!object.isSacrifice) || (!!object.valueBonus) || (!!object.valueMultiply)
				})
				this.bodyArray.push(body)

				if (object.portalID) {
					body.collisionFilter.mask = 0
					this.portalMap[object.portalID] = body
				}

				let movable = false
				if (object.movable)
					movable = object.movable
				if (!movable)
					Matter.Body.setStatic(body, true)

				if (movable && object.constraint) {
					let cx = object.constraint.pos[0]
					let cy = object.constraint.pos[1]
					let constraint = Matter.Constraint.create({
						bodyA: body,
						pointA: { x: cx, y: cy },
						pointB: { x: object.x + cx, y: object.y + cy }
					})
					this.bodyArray.push(constraint)
				}
			} else if (object.type == 'marbles') {
				this.startX = object.x
				this.startY = object.y
				this.passX = object.passX
				this.passY = object.passY
				this.launchTime = object.launchTime
				this.launchRadius = object.launchRadius
				this.launchVelocity = object.velocity
				this.marbleRadius = object.radius
				for (let i = 0; i < this.n; ++i)
					if (this.marbleStatus[i].enabled || (this.enableEliminatedMarbles && this.marbleStatus[i].status == marbleRaceConfig.constant.ELIMINATED)) {
						let x = this.startX, y = this.startY
						if (this.marbleStatus[i].status == marbleRaceConfig.constant.PASSED && this.passX && this.passY) {
							x = this.passX
							y = this.passY
						}
						let circle = Matter.Bodies.circle(x, y, this.marbleStatus[i].status == marbleRaceConfig.constant.ELIMINATED ? object.eliminatedRadius : object.radius)

						let theta = this.random(0, Math.PI + Math.PI)
						Matter.Body.setVelocity(circle, Matter.Vector.create(this.launchVelocity * Math.cos(theta), this.launchVelocity * Math.sin(theta)))

						// set collision
						if (this.collisionConfig == 'none') {
							circle.collisionFilter.group = -1;
						}

						// set elasticity
						if (this.elastic) {
							circle.restitution = this.elastic
						}

						let size = (this.marbleStatus[i].status == marbleRaceConfig.constant.ELIMINATED ? object.eliminatedRadius : object.radius) * 2

						this.objectArray.push({
							body: circle,
							color: this.marbleConfigColor[i].color,
							size: size
						})
						this.bodyArray.push(circle)
						this.circleArray.push(circle)
						this.trailArray.push([])
						this.marbleSizeArray.push(size)
					} else {
						this.circleArray.push(undefined)
						this.trailArray.push(undefined)
						this.marbleSizeArray.push(undefined)
					}
			} else if (object.type == 'text') {
				this.textArray.push({
					content: object.content,
					color: object.color,
					x: object.x,
					y: object.y
				})
			}
		}

		for (let i = 0; i < this.objectArray.length; ++i) {
			let collider = this.objectArray[i]
			if (collider.hasCollision)
				for (let j = 0; j < this.circleArray.length; ++j)
					if (this.circleArray[j])
						Matter.Collision.create(collider, this.circleArray[j])
		}

		Matter.Composite.add(this.engine.world, this.bodyArray)

		this.runner = Matter.Runner.create()

		Matter.Events.on(this.render, 'beforeRender', function (event) { marbleRaceEngineMatter0190.beforeRender(event) })
		Matter.Events.on(this.render, 'afterRender', function (event) { marbleRaceEngineMatter0190.afterRender(event) })
		Matter.Events.on(this.runner, 'beforeUpdate', function (event) { marbleRaceEngineMatter0190.beforeUpdate(event) })
		Matter.Events.on(this.runner, 'afterUpdate', function (event) { marbleRaceEngineMatter0190.afterUpdate(event) })

		Matter.Render.run(this.render)
		Matter.Runner.run(this.runner, this.engine)
	}
}

marbleRaceEngines.push({
	name: 'Matter.js v0.19.0',
	id: 'Matter0190',
	engine: marbleRaceEngineMatter0190
})